#!/usr/bin/python

import datetime

from google.appengine.api import users
from google.appengine.ext import db
import thirdparty.simplejson as json

from hips import datastore


# Maps and constants for reading
# --------------------------------


CLASS_NAME_TO_CONSTRUCTOR_MAP = {
    "Institution": datastore.Institution,
    "Doctor": datastore.Doctor,
    "ScheduleBlock": datastore.ScheduleBlock,
    "Schedule": datastore.Schedule,
    "DoctorConstraint": datastore.DoctorConstraint,
    "ScheduleSolution": datastore.ScheduleSolution,
    "ScheduleSolutionBlock": datastore.ScheduleSolutionBlock}

CLASS_NAME_TO_SERIALIZED_ATTRIBUTES_MAP = {
    "Institution": ["name"],
    "Doctor": ["institution", "login", "name", "min_shifts", "max_shifts",
        "max_night_shifts", "max_consecutive_days"],
    "ScheduleBlock": ["schedule", "start_time", "duration_minutes"],
    "Schedule": ["institution", "start_time"],
    "DoctorConstraint": ["doctor", "schedule", "days_off", "min_shifts", 
        "max_shifts", "max_night_shifts", "max_consecutive_days"],
    "ScheduleSolution": ["schedule", "message"],
    "ScheduleSolutionBlock": ["solution", "block", "doctor"],
    }

# Used to read/write datetime format values.
DATETIME_FORMAT_STRING = "%Y-%m-%d %H:%M:%S"


# Reading attributes
# --------------------------------


def CreateUser(s):
  return users.User(email=s)


def CreateDatetime(s):
  return datetime.datetime.strptime(s, DATETIME_FORMAT_STRING)


def CreateKey(s):
  return db.Key(s)


# TODO Watch out for name collisions here
DESERIALIZATION_FUNCTION_MAP = {
    "login": CreateUser,
    "start_time": CreateDatetime,
    "institution": CreateKey,
    "doctor": CreateKey,
    "schedule": CreateKey,
    "solution": CreateKey,
    "block": CreateKey,
    }


def _DeserializeAttributes(model_class, attributes_map, attribute_names):
  deserialized = {}
  for name in attribute_names:
    if name not in attributes_map:
      # If we're missing required attributes, model creation will
      # throw an error.
      continue
    value = attributes_map[name]
    fn = DESERIALIZATION_FUNCTION_MAP.get(name, None)
    if fn:
      value = fn(value)
    deserialized[name] = value
  # TODO Keep "new" as marker or just use null key?
  if "key" in attributes_map and attributes_map["key"] != "new":
    # key_name is what GAE expects the key to be called.
    key = db.Key(attributes_map["key"])
    model = model_class.get(key)
    for attr_name, attr_value in deserialized.items():
      model.properties()[attr_name].__set__(model, attr_value)
  else:
    model = model_class(**deserialized)
  return model


def ReadModelListFromString(model_str):
  parsed_value = ParseJsonValue(model_str)
  if isinstance(parsed_value, list):
    return [ReadModelFromMap(m) for m in parsed_value]
  else:
    return [ReadModelFromMap(parsed_value)]


def ReadModelFromMap(attribute_map):
  # TODO Schedule blocks
  typename = attribute_map["type"]
  model_class = CLASS_NAME_TO_CONSTRUCTOR_MAP[typename]
  attribute_names = CLASS_NAME_TO_SERIALIZED_ATTRIBUTES_MAP[typename]
  model = _DeserializeAttributes(model_class,
      attribute_map, attribute_names)
  return model


# Writing models to string
# --------------------------------


SERIALIZATION_FUNCTION_MAP = {
    "login": (lambda x: x.email()),
    "start_time": (lambda x: x.strftime(DATETIME_FORMAT_STRING)),
    "institution": (lambda x: str(x.key())),
    "doctor": (lambda x: str(x.key())),
    "schedule": (lambda x: str(x.key())),
    "solution": (lambda x: str(x.key())),
    "block": (lambda x: str(x.key()))}


def _SerializeAttributes(model):
  # TODO Schedule blocks
  typename = model.__class__.__name__
  attribute_names = CLASS_NAME_TO_SERIALIZED_ATTRIBUTES_MAP[typename]
  serialized = {}
  for name in attribute_names:
    value = model.properties()[name].__get__(model, None)
    fn = SERIALIZATION_FUNCTION_MAP.get(name, None)
    if fn:
      value = fn(value)
    serialized[name] = value
  serialized["type"] = typename
  serialized["key"] = str(model.key())
  return serialized


def WriteModelToString(model):
  serialized_map = _SerializeAttributes(model)
  return ExportPythonMapToJson(serialized_map)


def WriteModelListToString(model_iter):
  model_str_list = [WriteModelToString(m) for m in model_iter]
  return "[%s]" % (", ".join(model_str_list))


# JSON handling helpers
# --------------------------------


def ParseJsonValue(json_value):
  return json.loads(json_value)


def ExportPythonMapToJson(python_map):
  return json.dumps(python_map)
